基于Qt的多线程TCP即时通讯软件的设计与实现

您所在的位置:网站首页 net 线程通信 基于Qt的多线程TCP即时通讯软件的设计与实现

基于Qt的多线程TCP即时通讯软件的设计与实现

#基于Qt的多线程TCP即时通讯软件的设计与实现| 来源: 网络整理| 查看: 265

基于Qt的多线程TCP即时通讯软件的设计与实现 前言Demo演示设计与开发基本技术学习与验证QTcpServer 与 QTcpSocketQTcpServerQTcpSocket 多线程多线程基础信号槽与多线程多线程TCP 通讯设计客户端设计界面设计网络设计 服务器设计服务器架构设计 打包与部署缺陷与改进工程源码与代码仓库参考源

前言

本文将从涉及到主要技术开始,讲解使用Qt来实现一个支持多客户端链接的多线程TCP服务器及其客户端的设计与实现的解决方案。 注:本文使用的开发环境为Qt5.15.2, 使用MSVC2019_64编译器, C++11及以上

Demo演示

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

设计与开发

接下来我将会详细讲解客户端和服务端的设计与实现的关键细节。完整的源代码在文章最后。

基本技术学习与验证 QTcpServer 与 QTcpSocket

注:如果你已经熟知Qt的TCP套接字的使用可以跳过这一部分。 Qt的TCP套接字编程相较于传统的套接字编程更加的简单方便。而且,其与Qt自身的信号槽机制和多线程相结合使得异步无阻塞的套接字编程实现更加的简单。如果你了解TCP协议的基本流程,以及传统的TCP套接字编程,那么Qt更加简单的语法将会使你欲罢不能。

首先来解释一下,“为什么有一个QTcpServer,传统的不是只有一个socket吗?” 其实QTcpServer和QTcpSocket都是对原生的socket的封装,只是QTcpServer对服务器接收新连接的socket做了更好的处理。 如果是原生的socket,你一定也会创建两个socket,一个负责监听并处理新的socket接入,另一个根据监听来创建与客户端的TCP连接,使得服务器可以与客户端通讯。如果只有一个socket那么一个客户端接入之后服务端变失去了监听并接受新的连接的效能。 所以,在Qt中直接将这样的两种职能的socket封装成了QTcpServer与QTcpSocket。

QTcpServer

见名知意其是处理TCP套接字的服务器端,与套接字本身并无太大的区别,Qt对其的封装使得你可以更好的实现客户端套接字的接入处理,而不同于传统的套接字编程都是直接使用。通过下面的语句即可创建一个QTcpServer

QTcpServer *tcpServer = new QTcpServer();

创建完成一个服务端的套接字,为了使其监听指定的IP和端口,使用 listen()方法即可

tcpServer->listen(QHostAddress::Any, quint16(8848));

这样便可以监听从 任何IP 发到 本机8848端口 的TCP套接字 不同于传统的socket编程需要先绑定bind()再启动监听listen(),Qt将其封装在一起.

当有新的TCP套接字接入时,即有一个QTcpSocket请求连接时, 相应的QTcpServer将会自动创建一个与客户端连接的QTcpSocket的SocketDescriptor,并发出newConnection()信号。描述符你可以理解成标识码,即每个SocketDescriptor表示了不同的socket连接的具体信息,其本质是一个指针指向了一个QTcpSocket的核心数据块。

使用如下方法,即可让一个空的QTcpSocket完成配置,使其与客户端的socket连接。如果连接成功该QTcpSocket会发出一个connected()信号,客户端相应的socket也会发出同样的信号。

QTcpSocket *tcpSocket = new QTcpSocket(); tcpSocket = tcpServer->nextPendingConnection();

相应的信号槽可以这么写

connect(tcpServer, &QTcpServer::newConnection(), [&](){ tcpSocket = tcpServer->nextPendingConnection(); }); QTcpSocket

创建一个QTcpSocket,并发出连接请求

QTcpSocket *tcpSocket = new QTcpSocket(); tcpSocket->connectToHost(QHostAddress("127.0.0.1"), quint16(8848));

如果成功,那么tcpSocket将发出connected()信号。 发送数据使用write()方法即可

tcpSocket->write("Hello World!"); QString mesg = "Hello World!"; tcpSocket->write(mesg.toUtf8().data());

当服务器回送消息到达客户端后,客户端相应的QTcpSocket将会发出readyRead()信号,使用readAll()即可读取内容。

connect(tcpSocket, &QTcpSocket::readyRead, [&](){ QString mesg = tcpSocket->readAll(); });

消息传送完毕后,使用disconnectedFromHost()方法来断开TCP连接,客户端和服务端对应的套接字都会发出disconnected()信号。

tcpSocket->disconnectFromHost(); connect(tcpSocket, &QTcpSocket::disconnected, [&](){ qDebug() parent} { } void ChatThread::run() { emit showSignal(); }

这样你就自定义了一个线程,接下来在主线程中,创建线程,并使用start()启动它就行了

#include int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); ChatThread* chatThread = new ChatThread(); QObject::connect(chatThread, &ChatThread::showSignal, [&](){ qDebug() parent} { connect(this, &ChatBusiness::startSignal, this, &ChatBusiness::mainBusiness); }

在需要的地方创建,移入线程中,然后发出信号启动线程

void someFunction() { //some code //need that time cost process(ChatBusiness) ChatBusiness *chatBusiness = new chatBusiness(); QThread *thread = new thread(); chatBusiness->moveToThread(thread); emit chatBusiness->startSignal();//start the thread } 信号槽与多线程

下面这篇文章很好的阐释了信号槽与多线程以及事件循环的相关内容。 Qt 的线程与事件循环:https://blog.csdn.net/lynfam/article/details/7081757 简单来说,

connect()信号槽连接写在哪里不造成任何影响信号发出者与槽函数在同一个线程时, 采用Qt::DirectConnection,立即直接调用信号发出者与槽函数不在同一个线程时, 采用Qt::QueuedConnection,异步调用

而我们需要实现的多线程TCP就是属于第二类。

多线程TCP

我们这里只是实现简单的多线程TCP服务器。就是说新的链接接入,就为其创建一个线程单独处理。所以我们需要自定义新链接接入的处理逻辑。 这一点可以通过继承QTcpServer,实现自己的MyTcpServer并重写incomingConnection(qintptr handle)来实现

void MyTcpServer::incomingConnection(qintptr handle) { //create subThreaad QThread *thread = new QThread(this); ChatBusiness *chatBusiness = new ChatBusiness(); chatBusiness->moveToThread(thread); //def handle of start connect(chatBusiness, &ChatBusiness::start, chatBusiness, &ChatBusiness::mainBusiness); thread->start(); //send the SocketDescriptor emit chatBusiness->start(handle); } void ChatBusiness::mainBusiness(qintptr handle) { QTcpSocket *tcpSocket = new QTcpSocket(this); tcpSocket->setSocketDescriptor(handle); }

上面代码中,在子线程中业务处理类的处理开始是mainBusiness(qintptr handle)方法,其将传递的SocketDescriptor赋给创建的socket

至此你便了解所有的基本核心技术

通讯设计

从功能演示中我们看到,用户通过客户端发送自己的昵称,服务器进行认证注册;用户输入发送的目标,点击发送,服务器转发消息到相应的用户。其通讯流程如下图设计: 通讯设计序列图 由于是直接使用的TCP通讯,而不是应用层协议,需要自己根据需要设计通讯格式。 在本程序中通讯格式设计成:

服务器回送系统消息: [username,15]@[Server,15] [ServerMesgType,20]

ServerMesgType解释RegisterSuccessful名称注册成功RegisterFailed名称注册失败,重名TargetOffline接收方不在线

服务器转发用户消息: [targetName,15]@[username,15] [Mesg]

用户发送系统消息(注册名称): [username,15]@[Server,15] [ServerMesgType,20]

ServerMesgType解释Register请求注册名称 客户端设计 界面设计

登录窗口 登录窗口 聊天主窗口 主聊天窗口 此外为了保证UI显示在不同分辨率的显示器、不同的缩放比例下有着较好的适配,我们需要如下设置

添加新的qrc在这里插入图片描述 在这里插入图片描述 然后在文件夹中创建 /etc/qt.conf 文件 在这里插入图片描述 qt.conf 文件中添加如下内容

[Platforms] WindowsArguments = dpiawareness=0 WindowsArguments = fontengine=freetype

第一行是让程序的缩放程度跟随系统控制 第二行是使用freetype字体,使得缩放时的字体不会出现明显的锯齿

同时在main.cpp文件中添加,来使得缩放时图像不会有锯齿

QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);

在这里插入图片描述

网络设计

客户端的网络实现比较简单,只需要使用QTcpSocket。实现其readyRead(),connected(),disconnected()信号的处理即可。 这里不再赘述。

服务器设计

无论从功能需要还是性能要求,服务端做成控制台应用即可,无需使用Qt的界面。

服务器架构设计

服务器架构设计-类图 即:

MyTcpServer作为服务器主要的调度,接受新的链接、注册名称、转发消息重写M有TCPServer的incomingConnection(qintptr handle)方法,创建ChatBusiness业务对象,并移入子线程启动ChatBusiness创建对应的MyTcpSocket套接字与客户端连接实现自己的MyTcpSocket使其之间内部就可以处理消息的发送,转发系统请求到ChatBusiness

这样客户端和服务器的设计和实现细节便全部描述完毕。你可以一次尝试自己开发,或者在文章的最后部分下载工程源码

打包与部署

在完成编码后我们需要测试,或者给朋友玩玩之类,就需要对程序打包部署。 首先我们以Release模式重新构建项目,然后将构建好的对应的.exe文件拷贝到一个单独的文件夹 例如.../Client/client-Demo.exe

然后在Qt的 安装目录 找到项目使用的对应的 编译器文件夹 例如: 编译器文件夹 确认有该工具 windeployqt 在该目录打开命令行,输入.\windeployqt.exe Client.exe的完整路径,例如

.\windeployqt.exe C:\Client\Client-Demo2.exe

之后使用Enigma Virtual Box打包即可,如果在其他电脑上不能运行。。。那就根据提示再打包。例如提示找不到xxxx.dll,就把该dll放到Client.exe同级文件夹下,再使用Enigma Virtual Box打包。 嗯,是的,感觉很是愚蠢。但是这是相对来说比较省事的方法了。

如果涉及Qt之外的库,那就需要用depends.exe来查找依赖了。 可以参考这篇文章:QT+Opencv 程序打包发布:https://blog.csdn.net/qq_43599883/article/details/106251915

缺陷与改进 客户端交互体验较差。没有Enter快捷键;无法切换聊天窗口等。客户端界面不够美观。没有使用QSS等进行界面设计。无法传输图像等多媒体数据。服务器性能设计不足。一个socket一个线程有点过于奢侈。服务器没有存储消息等数据存储能力,应该与数据库结合实现。明文传输数据。应当考虑使用https传输,或者自己实现AES对称加密消息,RSA加密密钥来传输。TCP连接链路拥塞。应当使用QTimer来发送“心跳”包确认链路通畅,以避免连接中断而disconnected()信号没有发送的情况。开发中没有使用Github等工具合理的管理程序版本。实现过程不够规范。缺少对类和类之间关系的建模、文档模型化,导致编写时常常忘记链接信号槽、实现相应的槽函数等。(身为软件工程出身这太不应该,欲速则不达〒▽〒)客户端连接接服务器的IP不可变化。软件部署方式难以更新。额,欢迎提出建议…

部分内容已经在下一个版本的开发日程中,之后会在本博客中更新新的工程源码,欢迎持续关注。ヾ(≧▽≦*)o

工程源码与代码仓库

版本1:https://github.com/SWULWJ/AirChat 版本2:AirChat2.0

参考源

QT 使用全局缩放进行全分辨率适配(QT_SCALE_FACTOR):https://blog.csdn.net/u014410266/article/details/107488789 Qt Windows高清DPI自适应分辨率缩放:https://blog.csdn.net/startl/article/details/105862817 Qt 的线程与事件循环:https://blog.csdn.net/lynfam/article/details/7081757 Qt多线程中的信号与槽:https://blog.csdn.net/qq_29344757/article/details/78136829 TCP_UDP_Assistant.zip (一个很厉害的例子):https://download.csdn.net/download/yxy244/12030948 QT+Opencv 程序打包发布:https://blog.csdn.net/qq_43599883/article/details/106251915 Qt应用程序在windows和Linux操作系统下的打包发布:https://blog.csdn.net/qq_41488943/article/details/104963916

RSA算法原理:https://zhuanlan.zhihu.com/p/48249182 AES加密算法的详细介绍与实现:https://blog.csdn.net/qq_28205153/article/details/55798628

- - - - AirChat2.0 - - - - Qt–堆栈窗口(QStackedWidget)的使用:https://blog.csdn.net/trailbrazer/article/details/54344590 QT5 使用163邮箱发送邮件:https://blog.csdn.net/wangdeyu1994/article/details/78693427 Qt. QSqlDatabase Class Documentation: https://doc.qt.io/qt-5/qsqldatabase.html

低劣的转载真的太多了。。。

感谢阅读!

有疑问或者认为有错误请留言,谢谢!



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3